/*
// Exepack.NET
// http://www.codeplex.com/exepack
//
// Executable compressor (conceptual version)
// Written by Y [10-01-09]
// Copyright (c) 2008-2010 Alexey Yakovlev
//
// csc exepack.cs /res:loader.cs
*/

using System;
using System.IO;
using System.IO.Compression;
using System.Text;
using System.Reflection;
using System.Collections.Generic;
using System.CodeDom.Compiler;
using System.Drawing;
using Microsoft.CSharp;

static class ExePack
{
	static Dictionary<string, Assembly> loadedAssemblies = new Dictionary<string, Assembly>();
	static List<string> processedAssemblies = new List<string>();

	static int Main(string[] args)
	{
		Console.WriteLine("Exepack.NET, version 0.01 alpha{0}Copyright (c) 2008-2010 Alexey Yakovlev{0}", Environment.NewLine);

		if (args.Length != 1 || args[0].StartsWith("-") || args[0].IndexOf("?") >= 0)
		{
			Console.WriteLine("Syntax: exepack filename.exe");
			return 1;
		}

		return CompressExecutable(args[0]);
	}

	static int CompressExecutable(string fileName)
	{
		Console.WriteLine("Compressing executable: {0}", fileName);
		
		if (!File.Exists(fileName))
		{
			Console.WriteLine("File not found: {0}", fileName);
			return 2;
		}

		// get references
		List<string> asms = GetAssemblyWithReferences(fileName);

		Console.WriteLine("Compressing assemblies:");
		foreach (string asm in asms)
		{
			Console.WriteLine("\t{0}", asm);
		}

		// compress all files
		List<string> files = CompressFiles(asms);
		string iconName = ExtractIcon(fileName);
		string exeName = CreateExecutable(fileName, files, iconName, IsConsoleApplication(fileName));

		// delete temporary files
		files.Add(iconName);
		DeleteCompressedFiles(files);

		Console.WriteLine("Created compressed executable file: {0}", exeName);
		return 0;
	}

	static string ExtractIcon(string fileName)
	{
		string iconName = "exepack-saved-icon.deflated";

		try
		{
			Icon ico = Icon.ExtractAssociatedIcon(fileName);
			using (FileStream fs = File.Create(iconName))
			{
				ico.Save(fs);
			}

			return iconName;
		}
		catch
		{
			Console.WriteLine("Warning: could not extract exe-file icon");
			return null;
		}
	}

	static bool IsConsoleApplication(string fileName)
	{
		using (FileStream fs = File.OpenRead(fileName))
		using (BinaryReader reader = new BinaryReader(fs))
		{
			// read PE offset
			fs.Position = 0x3C;
			uint peOffset = reader.ReadUInt32();

			// jump to PE header subsystem flag
			fs.Position = peOffset + 0x5c;
			byte subsystem = reader.ReadByte();

			// 2 = GUI, 3 = console
			return subsystem == 3;
		}
	}

	static void DeleteCompressedFiles(List<string> files)
	{
		foreach (string fileName in files)
		{
			if (!String.IsNullOrEmpty(fileName))
				File.Delete(fileName);
		}
	}

	static string CreateExecutable(string exeName, List<string> files, string iconName, bool isConsole)
	{
		// prepare loader source code
		string loader = GetLoaderSource(files[0]);

		// prepare compiler parameters
		CompilerParameters cp = new CompilerParameters();
		cp.GenerateExecutable = true;
		cp.OutputAssembly = Path.ChangeExtension(exeName, "packed.exe");
		cp.IncludeDebugInformation = false;
		cp.GenerateInMemory = false;
		cp.ReferencedAssemblies.Add("System.dll");
		cp.CompilerOptions = "/o /filealign:512";
		cp.CompilerOptions += isConsole ? " /target:exe" : " /target:winexe";
		cp.CompilerOptions += iconName != null ? " /win32icon:\"" + iconName + "\"" : "";

		// add compressed resources
		foreach (string fileName in files)
		{
			if (fileName == exeName)
				continue;

			cp.EmbeddedResources.Add(fileName);
		}

		// compile and check for errors
		CompilerResults cr = new CSharpCodeProvider().CompileAssemblyFromSource(cp, loader);
		if (cr.Errors.Count > 0)
		{
			// Display compilation errors.
			Console.WriteLine("Errors building loader into {0}", cr.PathToAssembly);
			foreach (CompilerError ce in cr.Errors)
			{
				Console.WriteLine("  {0}", ce.ToString());
				Console.WriteLine();
			}
		}

		// return new executable name
		return cp.OutputAssembly;
	}

	static string GetLoaderSource(string exeResourceName)
	{
		using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Exepack.loader.cs"))
		{
			string loader = Encoding.UTF8.GetString(new BinaryReader(stream).ReadBytes((int)stream.Length));
			loader = loader.Replace("app, version=0.0.0.0, culture=neutral, publickeytoken=null", DecodeName(exeResourceName));
			return loader;
		}
	}

	static List<string> CompressFiles(List<string> asms)
	{
		List<string> files = new List<string>();

		foreach (string asmName in asms)
		{
			files.Add(CompressFile(asmName));
		}

		return files;
	}

	static string DecodeName(string resName)
	{
		return Encoding.Default.GetString(Convert.FromBase64String(resName));
	}

	static string EncodeName(string asmName)
	{
		return Convert.ToBase64String(Encoding.Default.GetBytes(asmName.ToLowerInvariant()));
	}

	static string CompressFile(string asmName)
	{
		// load assembly
		Assembly asm = LoadAssembly(asmName);
		string compressedName = EncodeName(asm.GetName().ToString());

		using (FileStream inFile = File.OpenRead(asm.Location))
		using (FileStream outFile = File.Create(compressedName))
		using (DeflateStream ds = new DeflateStream(outFile, CompressionMode.Compress))
		{
			var w = new BinaryWriter(outFile);
			w.Write((int)inFile.Length);
			w.Flush();

			w = new BinaryWriter(ds);
			w.Write(new BinaryReader(inFile).ReadBytes((int)inFile.Length));
			w.Flush();
		}

		return compressedName;
	}

	static List<string> GetAssemblyWithReferences(string asmName)
	{
		List<string> result = new List<string>();

		// check if already processed
		if (processedAssemblies.Contains(asmName))
		{
			return result;
		}

		// load assembly
		Assembly asm = LoadAssembly(asmName);
		processedAssemblies.Add(asm.FullName);
		result.Add(asm.FullName);

		// get assembly references
		AssemblyName[] refs = asm.GetReferencedAssemblies();
		foreach (AssemblyName rn in refs)
		{
			if (!IsSystemAssembly(rn))
			{
				// add references recursively
				List<string> refRefs = GetAssemblyWithReferences(rn.FullName);
				foreach (string refRef in refRefs)
					result.Add(refRef);
			}
		}

		return result;
	}

	static bool IsSystemAssembly(AssemblyName rn)
	{
		string name = rn.Name.ToLowerInvariant();
		return rn.GetPublicKeyToken() != null && (name.StartsWith("system") || name.StartsWith("mscor"));
	}

	static Assembly LoadAssembly(string name)
	{
		name = name.ToLower();

		if (!loadedAssemblies.ContainsKey(name))
			try
			{
				loadedAssemblies[name] = Assembly.ReflectionOnlyLoadFrom(name);
			}
			catch (System.IO.FileNotFoundException)
			{
				try
				{
					loadedAssemblies[name] = Assembly.Load(name);
				}
				catch (System.IO.FileNotFoundException)
				{
					Console.WriteLine("Warning: could not load assembly: {0}", name);
					return null;
				}
			}

		return loadedAssemblies[name];
	}
}
